# Copyright 2025 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Performance regression test for the compile commands database merger."""

import os
import subprocess
import tempfile
import time
import unittest

from python.runfiles import runfiles  # type: ignore

# This is generated by the pw_py_importable_runfile rules in the BUILD.bazel
# file.
from pw_ide import update_compile_commands_binary

# The maximum allowed time for a nop build of the compile commands
# database. This is primarily to catch substantial regressions in the aspect
# logic.
_MAX_NOP_TIME_SECONDS = 10.0


# pylint: disable=line-too-long
_TEST_CPP_TARGET = (
    '//pw_ide/bazel/compile_commands/test:test_compile_commands_outputs'
)
# pylint: enable=line-too-long

# If you want to change this target, pick something that pulls in the Pico SDK,
# which combine with some Pigweed libraries should create a big enough target
# that hopefully catches performance regressions.
_BIG_TEST_CPP_TARGET = '//pw_system:rp2040_system_example'


class PerformanceTest(unittest.TestCase):
    """A base class with tests for compile commands integration."""

    temp_dir: tempfile.TemporaryDirectory
    project_root: str

    @classmethod
    def setUpClass(cls):
        cls.runfiles = runfiles.Create()
        cls.updater_path = cls.runfiles.Rlocation(
            *update_compile_commands_binary.RLOCATION
        )
        assert 'BUILD_WORKSPACE_DIRECTORY' in os.environ, (
            'This must be `bazel run` to work properly, and cannot be tested '
            'via `bazel test`'
        )
        cls.project_root = os.environ.get('BUILD_WORKSPACE_DIRECTORY')
        cls.temp_dir = tempfile.TemporaryDirectory()

    @classmethod
    def tearDownClass(cls):
        cls.temp_dir.cleanup()

    def test_performance_is_acceptable(self):
        """Checks that the aspect does not take too long to run."""
        # Run once to warm the cache.
        warmup_result = subprocess.run(
            [
                self.updater_path,
                f'--out-dir={self.temp_dir.name}',
                '--',
                'build',
                _BIG_TEST_CPP_TARGET,
            ],
            capture_output=True,
            text=True,
            check=False,
        )
        if warmup_result.returncode != 0:
            self.fail(
                'update_compile_commands warmup failed: '
                f'{warmup_result.stderr}'
            )

        # Launch another Bazel command that shouldn't invalidate
        # the cached artifacts, but may convince bazel to redo some of the
        # work at the aspect/starlark level.
        different_build = subprocess.run(
            [
                'bazelisk',
                'build',
                _TEST_CPP_TARGET,
            ],
            cwd=self.project_root,
            capture_output=True,
            text=True,
            check=False,
        )
        if different_build.returncode != 0:
            self.fail('Alternate build failed: ' f'{different_build.stderr}')

        # This second update run should not take a long time.
        start_time = time.time()
        update_result = subprocess.run(
            [
                self.updater_path,
                f'--out-dir={self.temp_dir.name}',
                '--',
                'build',
                _BIG_TEST_CPP_TARGET,
            ],
            capture_output=True,
            text=True,
            check=False,
        )
        duration = time.time() - start_time

        if update_result.returncode != 0:
            self.fail(
                'update_compile_commands failed: ' f'{update_result.stderr}'
            )

        self.assertLess(
            duration,
            _MAX_NOP_TIME_SECONDS,
            f'Compile commands update took {duration:.2f}s, which exceeds the '
            f'maximum permitted duration of {_MAX_NOP_TIME_SECONDS:.2f}',
        )


if __name__ == '__main__':
    unittest.main()
